package com.spoorle.perf.impl;

import java.io.StringWriter;
import java.util.Date;
import java.util.Enumeration;
import java.util.GregorianCalendar;
import java.util.Hashtable;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;

import javax.xml.bind.JAXBContext;
import javax.xml.bind.JAXBException;
import javax.xml.bind.Marshaller;
import javax.xml.datatype.XMLGregorianCalendar;

import com.spoorle.perf.iface.Probe;
import com.spoorle.perf.iface.Statistics;
import com.spoorle.perf.impl.bind.ObjectFactory;
import com.spoorle.perf.impl.bind.SJMPOperation;
import com.spoorle.perf.impl.bind.SJMPStatistic;
import com.spoorle.perf.impl.bind.SJPMResult;
import com.sun.org.apache.xerces.internal.jaxp.datatype.XMLGregorianCalendarImpl;
/**
 * <p>This is the default implementation of Probe.</p>
 * @author Maciej Grula <a href="mailto:maciej.grula@gmail.com">maciej.grula@gmail.com</a>
 */
public class DefaultProbeImpl implements Probe {
	/**
	 * class for which probing is used
	 */
	private Class clazz;
	
	/**
	 * Storage of statistics data
	 */
	private static Hashtable<String, Statistics> statisticsHashtable;
	
	/**
	 * <p>Storage for statistics during collection (open or running statistics). When start operation
	 * is invoked then a new item is added to this table. When stop operation is invoked then 
	 * proper item is found and removed from table.</p> 
	 * @see OngoingRunDataHolder
	 */
	private static Hashtable<String, OngoingRunDataHolder> runningStatistics;
	
	/**
	 * when false then no new stats are gathered
	 */
	private static boolean enabled;
	
	/**
	 * Initializes storage tables.
	 * @param clazz
	 */
	public DefaultProbeImpl(Class clazz){
		if(runningStatistics==null)
			runningStatistics = new Hashtable<String, OngoingRunDataHolder>();
		if(statisticsHashtable==null)
			statisticsHashtable = new Hashtable<String, Statistics>();
		this.clazz = clazz;
	}
	/**
	 * Creates holder for operation and stores it in the runningStatistics table.
	 */
	public void start(String operation) {
		if(enabled){
			OngoingRunDataHolder holder = getHolder(operation);
			runningStatistics.put(keyForHolder(holder), holder);
		}
	}
	
	/**
	 * Measures time. Finds holder in runningStatistics table. When there is no holder then
	 * it's assumed that stop was invoked . Otherwise holder is retrieved and used to calculate 
	 * new statistics object. Statistics is stored in statistics table. 
	 */
	public void stop(String operation) {
		long currentTime = System.currentTimeMillis();
		if(statisticsHashtable==null)
			return;
		// to get key value
		OngoingRunDataHolder holder = getHolder(operation);
		// retrieve when exists and remove from running statistics
		holder = runningStatistics.remove(keyForHolder(holder));
		// there is no such running statistic so skip
		if(holder==null)
			return;
		
		// we have statistics that were run so trying to add statistics
		
		// no negative values allowed, it is assumed that current time is always greater then start time
		long timeDifference = Math.max(currentTime-holder.startTime,0);
		
		// current sample
		DefaultStatistcsImpl currentSample = new DefaultStatistcsImpl();
		currentSample.setAvgVal(timeDifference);
		currentSample.setMaxVal(timeDifference);
		currentSample.setMinVal(timeDifference);
		currentSample.setCount(1);
		currentSample.setClazz(holder.clazz);
		currentSample.setOperation(holder.operation);
		
		// current statistics
		Statistics currentStatistics = statisticsHashtable.get(keyForStatistics(currentSample));

		if(currentStatistics!=null){
			// statistics exist so update
			
			// new, updated statistics
			DefaultStatistcsImpl newStatistics = new DefaultStatistcsImpl();
			// weighted average
			newStatistics.setAvgVal(((currentStatistics.getAvgVal()*currentStatistics.getCount())+(currentSample.getAvgVal()*currentSample.getCount()))/(currentSample.getCount()+currentStatistics.getCount()));
			newStatistics.setMaxVal(Math.max(currentStatistics.getMaxVal(), currentSample.getMaxVal()));
			newStatistics.setMinVal(Math.min(currentStatistics.getMinVal(), currentSample.getMinVal()));
			newStatistics.setCount(currentStatistics.getCount()+currentSample.getCount());
			newStatistics.setOperation(currentSample.operation);
			newStatistics.setClazz(currentSample.getClazz());
			
	
			statisticsHashtable.put(keyForStatistics(newStatistics), newStatistics);
		}else{
			// create statistics as there is no such statistics
			statisticsHashtable.put(keyForStatistics(currentSample), currentSample);
		}
	}
	
	/**
	 * <p>Instantiates holder object for given operation. Uses clazz property of this probe instance and current thread id.</p> 
	 * @param operation textual name for operation
	 * @return
	 */
	protected OngoingRunDataHolder getHolder(String operation){
		OngoingRunDataHolder runHolder = new OngoingRunDataHolder();
		runHolder.clazz = this.clazz;
		runHolder.operation = operation;
		runHolder.startTime = System.currentTimeMillis();
		runHolder.threadId = Thread.currentThread().getId();
		return runHolder;
	}
	
	/**
	 * Calculates hashtable key for given holder. Concatenates clazz, threadId and operation as a key.  
	 * @param holder
	 * @return
	 */
	protected String keyForHolder(OngoingRunDataHolder holder){
		return ""+holder.clazz+holder.threadId+holder.operation;
	}
	
	/**
	 * Calculates hashtable key for given statistic. Concatenates clazz and operation as a key.
	 * @param statistics
	 * @return
	 */
	protected String keyForStatistics(Statistics statistics){
		return statistics.getClazz()+statistics.getOperation();
	}

	public void disable() {
		enabled = false;
	}

	public void enable() {
		enabled = true;
	}

	public boolean isEnabled() {
		return enabled;
	}

	public List<Statistics> getStatistics() {
		if(statisticsHashtable==null)
			return null;
		LinkedList<Statistics> statisticsList = new LinkedList<Statistics>();
		
		for(Enumeration<Statistics> enumeration = statisticsHashtable.elements();enumeration.hasMoreElements();){
			statisticsList.add(enumeration.nextElement());
		}
		
		return statisticsList;
	}

	public Statistics getStatistics(String operation, Class clazz) {
		if(statisticsHashtable==null)
			return null;
		DefaultStatistcsImpl defaultStatistics = new DefaultStatistcsImpl();
		defaultStatistics.setOperation(operation);
		defaultStatistics.setClazz(clazz);
		return statisticsHashtable.get(keyForStatistics(defaultStatistics));
	}

	/**
	 * Generates XML representation of statistics.
	 * @see sjpm_results.xsd
	 */
	public String spool() {
		ObjectFactory factory = new ObjectFactory();
		
		SJPMResult sjmpResult=factory.createSJPMResult();
		
		GregorianCalendar gregorianCalendar = new GregorianCalendar();
		gregorianCalendar.setTime(new Date(System.currentTimeMillis()));
		XMLGregorianCalendar now = new XMLGregorianCalendarImpl(gregorianCalendar);
		
		sjmpResult.setCollectionDate(now);
		
		List<SJMPStatistic> sjmpStatistics = sjmpResult.getStatistic();
		
		for( Iterator<Statistics> statsIter = getStatistics().iterator();statsIter.hasNext();){
			Statistics stats = statsIter.next();
			SJMPStatistic sjmpStats = factory.createSJMPStatistic();
			sjmpStats.setClazz(stats.getClazz().getCanonicalName());
			sjmpStatistics.add(sjmpStats);
			
			List<SJMPOperation> sjmpOperations = sjmpStats.getOperation();
			SJMPOperation sjmpOperation = factory.createSJMPOperation();
			sjmpOperation.setName(stats.getOperation());
			sjmpOperation.setAverage(stats.getAvgVal());
			sjmpOperation.setCount((int)stats.getCount());
			sjmpOperation.setMaximum(stats.getMaxVal());
			sjmpOperation.setMinimum(stats.getMinVal());
			sjmpOperations.add(sjmpOperation);
		}
		
		JAXBContext jaxbContext;
		String result = "";
		try {
			jaxbContext = JAXBContext.newInstance("com.spoorle.perf.impl.bind");
			Marshaller marshaller=jaxbContext.createMarshaller();
			StringWriter stringWriter = new StringWriter();
			marshaller.setProperty("jaxb.formatted.output", true);
			marshaller.setProperty("com.sun.xml.bind.xmlHeaders", "<?xml-stylesheet type='text/xsl' href='foobar.xsl' ?>");

			marshaller.marshal(sjmpResult, stringWriter);
			result = stringWriter.toString();
		} catch (JAXBException e) {
			e.printStackTrace();
		}	
		return result;
	}
	
	public void release() {
		statisticsHashtable = new Hashtable<String, Statistics>();
		runningStatistics = new Hashtable<String, OngoingRunDataHolder>();
		enabled = false;
	}
}
